package com.gaojinqi.lock.redis;

import com.gaojinqi.lock.AbstractDistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.lang.NonNull;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * Redis 锁初步实现，可自动续租 <br/>
 * 可用于单机、集群，只保证一定的一致性不操作原子性，    <br/>
 * <p>
 * <p>
 * 注意：
 * 1.集群时可能出现一致性问题
 * 1.1.主从异步复制：主节点把锁未同步到从节点就挂了，从节点选举成主节点，则锁会永久丢失； <br/>
 * 1.2.集群脑裂：发生网络分区时，重新选举，在集群把这个旧主节点变成一个新的从节点之前，客户端未更新路由表，将锁在发生给旧的主节点，导致锁丢失
 * 2.单机、集群并且未保证原子操作
 * 2.1. 目前可用但在网络延迟很大的情况可能会出现问题，续租：CAS 删除key：CAD待优化
 *
 * @author gaojinqi
 * @version 1.0
 * @since 2020年07月7日
 */
@Slf4j
@Deprecated
public class RedisLock extends AbstractDistributedLock {

    @NonNull
    private StringRedisTemplate redisTemplate;

    public RedisLock(@NonNull StringRedisTemplate redisTemplate) {
        this(redisTemplate, null);
        this.redisTemplate = redisTemplate;
    }

    public RedisLock(@NonNull StringRedisTemplate redisTemplate, Executor executor) {
        super(executor);
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected boolean setIfAbsent(String realKey, String token, long expireTime, TimeUnit timeUnit) {
        Boolean acquired = redisTemplate.opsForValue().setIfAbsent(realKey, token, expireTime, timeUnit);
        return acquired != null && acquired;
    }

    @Override
    protected boolean compareAndSet(String realKey, String token, long expireTime, TimeUnit timeUnit) {
        /*
            当key刚好过期时，这里可能会出现续租别的key的情况，最好能使用CAS或lua保证原子性。
            这里续租时间都是小于key过期时间的，因此不存在上述问题，但是可能会因为网络延迟导致上述情况，
            因此最好使用CAS保证原子性
            todo 待优化 解决方案：CAS
         */
        if (Objects.equals(token, redisTemplate.opsForValue().get(realKey))) {
            Boolean success = redisTemplate.expire(realKey, expireTime, timeUnit);
            return success != null && success;
        }
        return false;
    }

    @Override
    protected boolean compareAndDelete(String realKey, String token) {
        /*
            当key过期时可能会出现原子性问题：获取key的时候没有过期，但是删除的时候过期了，误删了下一个key持有者的凭证。
            最好用lua脚本保证原子性，但lua脚本在集群支持不是很好。
            因为提供了续租功能，并不会出现key过期的情况，因此可以不用。
            todo 待优化 解决方案：CAD
        */
        if (Objects.equals(token, redisTemplate.opsForValue().get(realKey))) {
            // 释放锁
            Boolean success = redisTemplate.delete(realKey);
            return success != null && success;
        }
        return false;
    }

    @Override
    protected boolean compare(String realKey, String token) {
        return Objects.equals(token, redisTemplate.opsForValue().get(realKey));
    }

}
